Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: ack receiver timestamps #1992

Open
wants to merge 30 commits into
base: main
Choose a base branch
from

Conversation

sterlingdeng
Copy link

@sterlingdeng sterlingdeng commented Sep 19, 2024

This PR is the implementation that addresses #1988

Manual E2E Testing/Validation instructions: sterlingdeng#2

Surfaces the TransportParameters to the application
Adds ReceiverTimestamps data structure to the space and expose methods.
This commit processes the inbound ACK frames that contain timestamps.
This commit handles sending frames that contain timestamps
Surfaces timing information to the CC interface.
@djc
Copy link
Member

djc commented Sep 20, 2024

I don't think a feature makes sense for this, what's the motivation for that? Suggest squashing all of these commits because I don't think they make much sense on their own.

@billylindeman
Copy link

@djc I suggested using a feature bc it is a draft specification, perhaps we should name the feature after the specification draft-smith-quic-receive-ts https://www.ietf.org/archive/id/draft-smith-quic-receive-ts-00.html

@djc
Copy link
Member

djc commented Sep 21, 2024

@djc I suggested using a feature bc it is a draft specification, perhaps we should name the feature after the specification draft-smith-quic-receive-ts https://www.ietf.org/archive/id/draft-smith-quic-receive-ts-00.html

I don't think that's a good reason to make it a feature. The current API surface will still become part of our public API, and it just adds a bunch of complexity. We've added draft functionality before, should be fine.

@sterlingdeng
Copy link
Author

I can remove the feature flag in a follow up commit. Regarding squashing, my intention for the commits were to keep separate different parts of the PR for review and then squash at the end. If you want me to squash it now, I can do that too.

@djc
Copy link
Member

djc commented Sep 24, 2024

I can remove the feature flag in a follow up commit. Regarding squashing, my intention for the commits were to keep separate different parts of the PR for review and then squash at the end. If you want me to squash it now, I can do that too.

While we are fans of clean commit histories with atomic commits, I think this falls short of that because the commits are not conceptually independent -- it doesn't make sense to add configuration that doesn't do anything, for example.

@sterlingdeng sterlingdeng changed the title Receiver Timestamps feat: ack receiver timestamps Sep 26, 2024
Copy link
Member

@djc djc left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here's a bunch of mostly stylistic feedback.

quinn-proto/src/config.rs Outdated Show resolved Hide resolved
@@ -34,6 +34,20 @@ pub trait Controller: Send + Sync {
) {
}

#[allow(unused_variables)]
/// Packet deliveries were confirmed with timestamps information.
fn on_ack_packet(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are these actually changes you'd look to make to the on_ack() interface which you're adding a separate method for to keep it semver-compatible?

If so, should this default implementation call self.on_ack()?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yea, I did it this way to prevent any breaking changes if there were users of the on_ack method. Yea, I think it makes sense for the default implementation to all self.on_ack, and then we can remove the congestion.on_ack call from on_packet_acked and just have it call congestion.on_ack_packet.

quinn-proto/src/connection/mod.rs Outdated Show resolved Hide resolved
quinn-proto/src/connection/mod.rs Outdated Show resolved Hide resolved
quinn-proto/src/connection/mod.rs Outdated Show resolved Hide resolved
quinn-proto/src/frame.rs Outdated Show resolved Hide resolved
quinn-proto/src/frame.rs Outdated Show resolved Hide resolved
self.next_pn -= 1;
}

let delta = self.data.get_var().unwrap();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this safe?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This follows the same pattern done on AckIter, where unwrap is used.
I think it's considered safe because this is defined in the spec. If the value is expected to be a VARINT based on the spec but it isn't, we wouldn't be able to proceed in processing the packet.
Another option is to change the return type to be a tuple that includes an error.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't include a SAFETY comment here because the others didn't have them either.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's considered safe because this is defined in the spec.

What's in the spec doesn't matter. Quinn must not panic just because a peer is non-conformant. That would be a DoS vulnerability.

I imagine this is safe because scan_ack_timestamps_blocks validated the ranges already. If that was the intent, document it, even if we've failed to do so elsewhere.

quinn-proto/src/lib.rs Outdated Show resolved Hide resolved
Comment on lines 170 to 179
receive_timestamps_exponent: config
.ack_timestamp_config
.as_ref()
.map(|cfg| cfg.exponent),

max_recv_timestamps_per_ack: config
.ack_timestamp_config
.as_ref()
.map(|cfg| cfg.max_timestamps_per_ack),

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think these maybe shouldn't be separate Options, but a tuple wrapped in a single Option?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In this file, rather than using two individual parameters on the TransportParameters struct, I'm using the AckTimestampConfig and making max_timestamps_per_ack an Option. None signaling that the feature is disabled.
Exponent defaults to 0, which is what the spec states.

It could be worth proposing for the draft that the transport parameters related to this feature are grouped together similar to how PreferredAddress does it.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It could be worth proposing for the draft that the transport parameters related to this feature are grouped together similar to how PreferredAddress does it.

Yes, that would probably be good.

quinn-proto/src/frame.rs Show resolved Hide resolved
Comment on lines 170 to 179
receive_timestamps_exponent: config
.ack_timestamp_config
.as_ref()
.map(|cfg| cfg.exponent),

max_recv_timestamps_per_ack: config
.ack_timestamp_config
.as_ref()
.map(|cfg| cfg.max_timestamps_per_ack),

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In this file, rather than using two individual parameters on the TransportParameters struct, I'm using the AckTimestampConfig and making max_timestamps_per_ack an Option. None signaling that the feature is disabled.
Exponent defaults to 0, which is what the spec states.

It could be worth proposing for the draft that the transport parameters related to this feature are grouped together similar to how PreferredAddress does it.

self.next_pn -= 1;
}

let delta = self.data.get_var().unwrap();
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This follows the same pattern done on AckIter, where unwrap is used.
I think it's considered safe because this is defined in the spec. If the value is expected to be a VARINT based on the spec but it isn't, we wouldn't be able to proceed in processing the packet.
Another option is to change the return type to be a tuple that includes an error.

quinn-proto/src/frame.rs Outdated Show resolved Hide resolved
@@ -34,6 +34,20 @@ pub trait Controller: Send + Sync {
) {
}

#[allow(unused_variables)]
/// Packet deliveries were confirmed with timestamps information.
fn on_ack_packet(
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yea, I did it this way to prevent any breaking changes if there were users of the on_ack method. Yea, I think it makes sense for the default implementation to all self.on_ack, and then we can remove the congestion.on_ack call from on_packet_acked and just have it call congestion.on_ack_packet.

fn new(largest: u64, basis_instant: Instant, exponent: u64, mut data: &'a [u8]) -> Self {
// We read and throw away the Timestamp Range Count value because
// it was already used to properly slice the data.
let _ = data.get_var().unwrap();
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This unwrap is safe because the data byte slice was sliced using scan_ack_timestamp_blocks, which this get_var() was checked using ?.

self.next_pn -= 1;
}

let delta = self.data.get_var().unwrap();
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't include a SAFETY comment here because the others didn't have them either.

fn new(largest: u64, basis_instant: Instant, exponent: u64, mut data: &'a [u8]) -> Self {
// We read and throw away the Timestamp Range Count value because
// it was already used to properly slice the data.
let _ = data.get_var().unwrap();
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added a safety comment.

Copy link
Collaborator

@Ralith Ralith left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As noted in the issue, please work with the IETF to reserve IDs. To avoid interop problems, public implementations must not use unreserved IDs

Comment on lines +231 to +232
pub fn max_ack_timestamps(&mut self, value: VarInt) -> &mut Self {
self.ack_timestamps_config.max_timestamps_per_ack = Some(value);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
pub fn max_ack_timestamps(&mut self, value: VarInt) -> &mut Self {
self.ack_timestamps_config.max_timestamps_per_ack = Some(value);
pub fn max_ack_timestamps(&mut self, value: Option<VarInt>) -> &mut Self {
self.ack_timestamps_config.max_timestamps_per_ack = value;
  • What happens if someone specifies Some(0)? Document.
  • Is it useful to users for the quantity to be configurable, or should this just be a bool flag? We don't expose configuration for the maximum number of ACK ranges, for example.

.finish()
}
}

/// Parameters for controlling the peer's acknowledgements with receiver timestamps.
#[derive(Clone, Debug, PartialEq, Eq, Copy)]
pub struct AckTimestampsConfig {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If this is public, then the main config struct should have a single config setter that accepts an Option<AckTimestampsConfig>. If you want setters for individual fields directly on the main struct, then it's confusing for this to be public.

pn: u64,
now: Instant,
sent: Instant,
received: Option<Instant>,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

received should use a type other than Instant to represent that it is not comparable to sent.

@@ -227,6 +229,10 @@ pub struct Connection {
/// no outgoing application data.
app_limited: bool,

// Ack Receive Timestamps
// The timestamp config of the peer.
ack_timestamps_cfg: AckTimestampsConfig,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Option?

@@ -34,6 +34,21 @@ pub trait Controller: Send + Sync {
) {
}

#[allow(unused_variables)]
/// Packet deliveries were confirmed with timestamps information.
fn on_ack_packet(
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
fn on_ack_packet(
fn on_ack_timestamped(

fn default() -> Self {
Self {
packet_number: 0,
timestamp: Instant::now(),
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's weird for default() to return different values over time. Maybe encode the delta here instead? Or, do we even need a Default impl?

}
if let Some(v) = self.data.back() {
if packet_number <= v.packet_number {
warn!("out of order packets are not supported");
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will be very noisy on some real networks. It should be trace at most, or omitted entirely.

});
}

pub(crate) fn subtract_below(&mut self, packet_number: u64) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Document.

}
}

pub(crate) fn iter(&self) -> impl DoubleEndedIterator<Item = PacketTimestamp> + '_ {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Document order.

self.ack_timestamps_cfg = params.ack_timestamps_cfg;
if let Some(max) = params.ack_timestamps_cfg.max_timestamps_per_ack {
for space in self.spaces.iter_mut() {
space.pending_acks.set_receiver_timestamp(max.0 as usize);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Clamp this maximum to a reasonable value to guard against memory exhaustion attacks.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants